Skip to content

feat: polyrepo workspace mode — repo-scoped lanes, merge, resume, naming, and dashboard - #21

Merged
HenryLach merged 71 commits into
mainfrom
feat/polyrepo-support
Mar 16, 2026
Merged

feat: polyrepo workspace mode — repo-scoped lanes, merge, resume, naming, and dashboard#21
HenryLach merged 71 commits into
mainfrom
feat/polyrepo-support

Conversation

@HenryLach

Copy link
Copy Markdown
Owner

Summary

Adds workspace mode for multi-repo orchestration. Tasks can target specific repositories, lanes are allocated per-repo, merges run per-repo, and resume/abort operate across repo boundaries.

What's included (TP-001 through TP-012)

Core infrastructure:

  • Workspace config loading and execution context (workspace.ts)
  • Task repo routing with prompt/area/workspace-default precedence (discovery.ts)
  • External task folder path resolution for cross-repo task areas
  • Strict routing enforcement (routing.strict: true)

Execution:

  • Repo-scoped lane allocation with global lane numbering (waves.ts)
  • Per-repo worktree provisioning with cross-repo rollback
  • Operator-scoped naming for sessions, worktrees, branches, merge artifacts (naming.ts)

Merge:

  • Repo-scoped merge sequencing (merge.ts:mergeWaveByRepo)
  • Partial-success reporting with per-repo attribution
  • Shared merge failure policy helper (messages.ts:computeMergeFailurePolicy)

Persistence & Resume:

  • Schema v2 with repo-aware task/lane records (persistence.ts)
  • v1→v2 auto-upconversion (no on-disk rewrite)
  • Resume reconciliation across repos (resume.ts)
  • Blocked-counter double-count fix for resumed batches

Observability:

  • Dashboard repo filter, badges, per-repo merge sub-rows
  • Workspace-aware doctor diagnostics

Quality:

  • 290 tests (9,886 lines of test code)
  • Collision-resistant naming test suite (83 tests)
  • Polyrepo fixture workspace and regression suite

Stats

  • 42 files changed, +11,923 -467 (net +11,456)
  • ~84% test code, ~16% production code
  • Backward compatible: repo mode behavior unchanged

Validation

  • cd extensions && npx vitest run — 290/290 passing
  • node bin/taskplane.mjs help + doctor — clean

…ed modules

Tests were reading from the old monolith task-orchestrator.ts but functions
were refactored into taskplane/ modules (formatting.ts, execution.ts,
worktree.ts, messages.ts, etc). Updated source resolution to read from all
modules. Added vitest dual-mode wrapper to orch-direct-implementation.
Updated isOrchestratedMode tests to match simplified implementation.
Step 0: Refactor lane allocation model

Type changes:
- Add repoId?: string to LaneAssignment, AllocatedLane, PersistedLaneRecord

New functions in waves.ts:
- groupTasksByRepo(): deterministic grouping by resolvedRepoId
- generateLaneId(): repo-aware lane ID (lane-N or repoId/lane-N)
- generateTmuxSessionName(): repo-aware session names

Updated functions:
- assignTasksToLanes(): optional laneOffset + repoId params
- allocateLanes(): groups by repo, allocates per group, globally
  unique lane numbers, repo-aware laneId/tmuxSessionName

Downstream propagation:
- persistence.ts: persist repoId on lane records
- resume.ts: restore repoId when reconstructing AllocatedLane

Compatibility:
- Repo mode (no resolvedRepoId): all tasks in single group,
  identical output to pre-TP-004 behavior
- Lane numbers globally unique (engine.ts/resume.ts assumptions preserved)
- laneNumber-keyed lookups in engine.ts/formatting.ts unchanged
- Add resolveRepoRoot() helper: resolves repoId → absolute repo root path
  from workspace config, falls back to default repoRoot in repo mode
- Add resolveBaseBranch() helper: fallback chain (per-repo defaultBranch →
  detected current branch → batch baseBranch)
- Add workspaceConfig parameter to allocateLanes()
- Refactor Stage 3: loop over repo groups, call ensureLaneWorktrees() per
  group with group-specific repoRoot and baseBranch
- Add cross-repo rollback: on failure in repo group N, roll back all
  previously-created worktrees from groups 1..N-1
- Update Stage 4: build worktree lookup from per-repo results
- Remove duplicate function definitions from prior iteration
- Add 19 unit tests for new helpers (resolveRepoRoot, resolveBaseBranch,
  groupTasksByRepo, generateLaneId, generateTmuxSessionName)
- Zero new test failures (4 pre-existing failures unchanged)
…fix abort for workspace-mode sessions

- Add workspaceConfig parameter to executeWave() and pass to allocateLanes()
- Thread workspaceConfig from executeOrchBatch() and resumeOrchBatch()
- Fix selectAbortTargetSessions() to match workspace-mode session names
  (<prefix>-<repoId>-lane-<N>) in addition to repo-mode (<prefix>-lane-<N>)
- Source laneId from PersistedLaneRecord instead of reconstructing as
  lane-${laneNumber}, preserving repo dimension in workspace mode
- Verify cleanup is already repo-agnostic (listWorktrees uses prefix matching)
- Add 7 unit tests for workspace-mode abort behavior
…ycle

- Add section 9 to waves-lanes-and-worktrees.md covering workspace mode:
  repo grouping, lane identity format, per-repo worktree provisioning,
  cross-repo rollback, and abort compatibility
- Create .pi/local/docs/taskplane/polyrepo-support-spec.md (internal,
  gitignored) as comprehensive reference for finalized lane identity
  contract and repo-scoped worktree rules
- Verify messages.ts unaffected: uses numeric laneNumber (globally unique),
  not string laneId
- Add WorkspaceRoutingConfig.strict boolean field (default: false)
- Add TASK_ROUTING_STRICT fatal discovery error code
- Parse routing.strict from taskplane-workspace.yaml
- Enforce strict mode in resolveTaskRouting(): require explicit
  promptRepoId when strict is enabled, with actionable remediation
  guidance pointing users to ## Execution Target section
- Repo mode and permissive workspace mode behavior unchanged
- 19 new tests covering strict routing enforcement in discovery
- Tests: strict mode rejects tasks without promptRepoId (19.x)
- Tests: strict mode accepts tasks with valid promptRepoId (20.x)
- Tests: permissive mode behavior unchanged / non-regression (21.x)
- Tests: TASK_ROUTING_STRICT is classified as fatal (22.x)
- Tests: repo mode unaffected by strict routing (23.x)
- Tests: end-to-end strict routing via runDiscovery pipeline (24.x)
- All 87 routing tests pass
- Add groupLanesByRepo() to partition lanes by repoId
- Add mergeWaveByRepo() that runs per-repo merge loops with correct
  repo roots and base branches (via resolveRepoRoot/resolveBaseBranch)
- In repo mode, single group passthrough preserves existing behavior
- In workspace mode, each repo group merges independently; failures in
  one repo don't block merging in other repos
- Add RepoMergeOutcome type for per-repo attribution on MergeWaveResult
- Add repoId field to MergeLaneResult for post-merge branch cleanup
- Update engine.ts to call mergeWaveByRepo with workspaceConfig
- Update engine.ts post-merge branch cleanup to use per-lane repo root
- Update resume.ts merge calls to use mergeWaveByRepo with workspaceConfig
- Update resume.ts branch cleanup to use per-lane repo root
- Fix hardcoded 'into develop' message text in messages.ts
- Add merge-repo-scoped.test.ts (10 tests: grouping, ordering, types)
- All 216 tests pass
…p, tests

- Set repoId on MergeLaneResult in mergeWave() success and error paths
- Fix aggregate status logic in mergeWaveByRepo() to use lane-level evidence
  (anyLaneSucceeded/anyLaneFailed) instead of repo-level status, correctly
  classifying 'all repos partial' as global partial (not failed)
- Add 10 new test cases: status rollup edge cases (all-succeed, mixed,
  all-fail, all-partial, vacuous, error-only, error+success) and repoId
  propagation verification
…egate status

R002 finding #1: mergeWave() can return status='failed' with
failedLane=null for pre-lane setup errors (temp branch creation,
worktree creation). mergeWaveByRepo() previously only checked
failedLane !== null, missing these setup failures entirely.

Fix: Track anyRepoFailed flag based on groupResult.status !== 'succeeded'
(not just failedLane). This catches both lane-level failures AND setup
failures. firstFailureReason is populated with setup error context when
failedLane is null.

R002 finding #2: Updated test helper computeAggregateStatus to match
the real implementation (uses repoStatuses array instead of single
firstFailedLane). Added 4 new test cases for setup-failure scenarios:
- repo setup failure with no lanes → failed
- repo setup failure + other repo success → partial
- all repos setup failure → failed
- repo setup failure + other repo partial → partial

All 207 tests pass.
… outcomes

Step 1 implementation:
- Add formatRepoMergeSummary() shared helper in messages.ts
- Add orchMergePartialRepoSummary template to ORCH_MESSAGES
- Wire partial-summary emission in both engine.ts and resume.ts
- Emit repo-divergence summary only when status=partial AND
  repoResults show different statuses across repos
- No misleading repo-divergence text for mixed-outcome-lane partials
- 8 new test assertions covering: divergence formatting, mono-repo,
  deterministic ordering, template usage, same-status suppression,
  single-group suppression, mixed-outcome-lane suppression
…y helper

Step 2: Extract computeMergeFailurePolicy() pure function into messages.ts.
Both engine.ts and resume.ts now use this shared helper to guarantee
identical pause/abort policy decisions, failure attribution, error
messages, and notifications on repo-scoped merge failures.

Changes:
- messages.ts: add MergeFailurePolicyResult type and computeMergeFailurePolicy()
- engine.ts: replace inline merge-failure handler with shared helper call
- resume.ts: replace inline merge-failure handler with shared helper call (parity fix)
- tests: 7 new test sections (19-25) covering pause/abort policy, setup failures,
  multi-lane attribution, engine/resume parity, reason truncation, determinism
…rator ID, fallback matrix, parser compat plan
…rees, branches, merge artifacts

- Create naming.ts with resolveOperatorId(), sanitizeNameComponent(), resolveRepoSlug()
- Add operator_id to OrchestratorConfig (auto-detected from OS username)
- Update TMUX sessions: {prefix}-{opId}-lane-{N} / {prefix}-{opId}-{repoId}-lane-{N}
- Update branches: task/{opId}-lane-{N}-{batchId}
- Update worktree dirs: {prefix}-{opId}-{N}
- Update merge: temp branch, workspace dir, session names, sidecar files
- Operator-scoped listWorktrees() with legacy fallback for opId='op'
- Propagate opId through waves.ts, worktree.ts, merge.ts, engine.ts, resume.ts
- Update tests for new naming patterns (207/207 passing)
- Created naming-collision.test.ts with 48 tests in 4 categories:
  2a: Collision test matrix (multi-operator, multi-repo, concurrent batches)
  2b: Ownership-safe consumer validation (parseOrchSessionNames, sidecar cleanup)
  2c: Human-readability validation (length budgets, token order, sort order)
  2d: Sanitization edge cases (case folding, truncation collision documentation)
- Documented prefix-only cleanup/abort as intended team behavior
- Documented sanitization collision risks (case folding, special char normalization, truncation)
- All 255 tests passing
Step 2: Validate collision resistance
- Collision matrix: operator × repo × batch × lane uniqueness for all artifact types
  (TMUX sessions, worktree paths, branches, merge temp branches, merge sidecars, merge sessions, merge workspace dirs)
- Shared-environment interference: parseOrchSessionNames prefix filtering, listWorktrees opId scoping,
  sidecar filename uniqueness, abort prefix-scoped kill (documented as intended team behavior)
- Human-readability: length bounds (TMUX ≤64, branches ≤100), token order consistency,
  safe character validation, provenance parseability, naming contract examples verification
- Naming utilities: sanitizeNameComponent, resolveOperatorId, resolveRepoSlug edge cases

All 290 tests passing (207 existing + 83 new).
…collectRepoRoots helper + mixed-repo tests

Step 0: Implement repo-aware reconciliation

- Verified that resume.ts already has repo-aware patterns for all 4 critical
  areas: reconnect polling, re-execute spawning, inter-wave worktree reset,
  and terminal worktree cleanup (from prior TP-005/TP-006 work)
- Added collectRepoRoots() helper function for collecting unique repo roots
  from persisted lane records (usable for test and future refactoring)
- Added 10 new tests for mixed-repo reconciliation scenarios:
  - Workspace v2: one repo lane alive + another dead
  - Workspace v2: .DONE in one repo + dead session in another
  - v1 state (no repo fields) reconciles correctly
  - Worktree exists vs missing split across repos
  - resolveRepoRoot integration (v2 vs v1/undefined)
  - collectRepoRoots for workspace and repo modes
  - computeResumePoint with mixed cross-repo outcomes
- Updated PersistedBatchStateForTest interface with mode/baseBranch fields
- All 290 tests passing across 12 test files
- Fix reconnect polling to use resolveRepoRoot() per-lane
- Fix re-execute spawning to use resolveRepoRoot() per-lane
- Fix inter-wave worktree reset to iterate unique repo roots
- Fix terminal worktree cleanup to iterate unique repo roots
- Add 8 mixed-repo reconciliation tests (section 8.1)
- v1 state files resume identically (undefined repoId → default root)
…d/skipped determinism

- Add 'pending' reconciliation action for never-started future-wave tasks
  (pending + no session) to prevent incorrect mark-failed classification
- Add 'skipped' to wave-skip condition in computeResumePoint (pre-existing gap)
- Fix blocked task counter double-counting with persistedBlockedTaskIds tracking
- Separate mark-complete from skip case in categorization for clarity
- Add 8 new test cases covering pending-vs-failed, skipped wave-skip,
  all-failed wave, counter stability, cross-repo blocked propagation,
  v1 fallback parity
- All 290 tests passing across 12 test files
…ng, blocked counter, repo attribution

Step 2 implementation:
- Fix re-exec merge indexing: use sentinel waveIndex -1, clamp persistence
  normalization with Math.max(0, ...) to prevent negative indices
- Fix blocked counter: count persisted-blocked tasks in unvisited waves
  at resume init (tasks blocked before their wave was entered were never counted)
- Fix repo attribution carry-forward: reconstructAllocatedLanes now accepts
  persistedTasks parameter to inject repoId/resolvedRepoId/taskFolder from
  prior state onto reconstructed AllocatedTask stubs
- Replace inline per-repo root collection loops with collectRepoRoots() helper
- Add 7 new tests covering checkpoint round-trip, blocked counter semantics,
  re-exec merge persistence, and mixed-repo metadata preservation
- All 290 tests passing across 12 test files
…ollectAllRepoRoots helper, 740 assertions passing
…passthrough

Step 0: Extend dashboard data model — complete.

- Add batch.mode field ('repo'|'workspace') to buildDashboardState()
  in dashboard/server.cjs so the frontend knows the workspace mode.
- Verified that lane repoId, task repoId/resolvedRepoId, and merge
  repoResults already flow through from persisted state (TP-006).
- All 290 tests passing, CLI smoke check OK.
- Additive-only changes — backward compatible.
…d repo-aware observability

- Add PersistedRepoMergeOutcome type for compact per-repo merge outcomes
- Add optional repoResults field to PersistedMergeResult interface
- Serialize MergeWaveResult.repoResults in serializeBatchState()
- Add validation for repoResults in validatePersistedState()
- Additive-only: absent/undefined in repo mode, backward compatible
…and merge grouping

- Add repo filter dropdown to header (index.html), hidden by default
- Add repo badge styles and merge sub-row styles (style.css)
- Implement buildRepoSet() to derive repos from lanes/tasks/mergeResults
- Implement updateRepoFilter() with disappearing-repo reset to 'All'
- Add repo badges to lane headers and task rows in renderLanesTasks()
- Add per-repo sub-rows in renderMergeAgents() for workspace mode
- Filter lanes/tasks/merge panels consistently by selected repo
- Gate all repo UI by mode=workspace AND 2+ distinct repos
- Summary bar and footer remain global (unfiltered)
- No changes to conversation/STATUS.md viewer panels
- Create polyrepo-builder.ts: runtime fixture with non-git workspace root,
  3 git repos (docs, api, frontend), shared task root, 3 task areas
- Define 6-task matrix with cross-repo deps spanning 3 waves:
  Wave 1: SH-001, AP-001, UI-001 (independent)
  Wave 2: AP-002, UI-002 (same-repo + cross-repo deps)
  Wave 3: SH-002 (cross-repo deps on wave 2)
- Add batch-state-v2-polyrepo.json static fixture for resume tests
- Add polyrepo-fixture.test.ts with 32 acceptance tests covering:
  topology, workspace config, discovery/routing, wave shape,
  static fixture validation, and ParsedTask builder helpers
- All 322 tests pass (including 32 new)
47 tests covering:
- /task routing with polyrepo discovery (4 tests)
- /orch-plan wave computation and lane allocation (7 tests)
- Serialization of repo-aware persisted state (4 tests)
- Per-repo merge outcomes and partial failures (4 tests)
- Resume reconciliation and resume-point for workspace mode (10 tests)
- Collision-safe naming across repos (8 tests)
- Repo-aware state validation and v1→v2 upconversion (10 tests)

All 369 suite tests pass.
… docs

- Create monorepo-compat-regression.test.ts with 34 tests guarding:
  - v1→v2 persistence upconversion (mode=repo, no repo fields)
  - Repo-mode discovery (no routing, no resolvedRepoId)
  - Repo-mode naming (no repoId segments in lane IDs/sessions)
  - Repo-mode merge grouping (single group for undefined-repoId lanes)
  - Repo-mode resume (mode-agnostic eligibility, proper reconstruction)
  - Repo-mode serialization round-trip (serialize → validate → no repo fields)
  - freshOrchBatchState defaults regression

- Update docs/maintainers/testing.md with:
  - Polyrepo fixture usage guide and when to use polyrepo vs monorepo tests
  - Fixture limitations (temp FS, no real git history, fixed topology)
  - Updated key files listing

All 403 tests pass across 15 test files.
@HenryLach
HenryLach force-pushed the feat/polyrepo-support branch from 2751d48 to e7954b0 Compare March 16, 2026 01:17
@HenryLach
HenryLach merged commit 190fdfa into main Mar 16, 2026
1 check passed
@HenryLach
HenryLach deleted the feat/polyrepo-support branch March 16, 2026 01:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant